﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Effects.Wave;
using Effects.GUI;

namespace Effects.GUI
{
    // Class provides data source for EffectStreamPlayer. It's not actually stream.
    /// <summary>
    ///  Input data source for <see cref="EffectStreamPlayer"/> class.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This class actually isn't stream in a common .NET way (It is not derived from any base Stream class).
    ///   It provides source of effected signal from input file. Any consumer can retrieve it by <see cref="EffectStream.Get"/> method. 
    ///  </para>
    ///  <para>
    ///   Class stores prepared data in array of doubles used as simple circular buffer. Data must be in buffer at the moment consumer asks for them
    ///   On the other hand changes in effect settings should be significant right after the user has made them. Therefore the data can't
    ///   be prepared too soon and stored for long time. To prepare data just before consumer asks for them <see cref="System.Timers.Timer"/> is used.
    ///   Every 5 miliseconds (or so) it calls method that controls status of the buffer and fills it if needed.
    ///  </para>
    /// </remarks>
    public class EffectStream
    {
        #region effects
        // effect boards 

        /// <summary>
        ///  <see cref="EffectBoard"/>s that are used to effect channels of the input signal.
        /// </summary>
        private EffectBoard[] effects;

        /// <summary>
        ///  Worksize for the effect board.
        /// </summary>
        private int worksize;

        #endregion

        #region input_wave
        // fields and prperties coopereting with input file

        // instance of Wave to be effected and buffered
        /// <summary>
        ///  Input <see cref="Wave"/> containing input signal.
        /// </summary>
        private Wave.Wave wav;

        // Property to acces FmtChunk
        /// <summary>
        ///  Gets <see cref="FmtChunk"/> of the wrapped Wave.
        /// </summary>
        public FmtChunk FormatChunk
        {
            get { return wav.FmtChunk; }
        }

        /// <summary>
        ///  Channels count of the input.
        /// </summary>
        private int channels;

        /// <summary>
        ///  Read index to the <see cref="EffectStream.wav"/>
        /// </summary>
        private int readPosition;

        /// <summary>
        ///  Gets current read position to the file.
        /// </summary>
        public int ReadPosition
        {
            get { return readPosition; }
        }

        /// <summary>
        ///  Gets or sets current play position.
        /// </summary>
        /// <remarks>
        ///  This property differs from <see cref="EffectStream.ReadPosition"/> property. It compensates the samples that are allready buffered. 
        ///  So returned value points at the position of the sample that will be returned in Get method.
        /// </remarks>
        public int Position
        {
            get { return readPosition - bufferdCount; }
            set
            {
                if (value < 0 || value > Length)
                    throw new ArgumentException("Position out of range. Given value must be greater than 0 and less than Length");
                // set new position and buffer
                this.readPosition = value;
                lock (locker)
                {
                    // buffered = new List<double>();
                    // toBuffer = bufferSize;
                    bufferArray = new double[bufferSize];
                    toBuffer = bufferSize / 20;
                    FillBuffer();
                }
            }
        }

        /// <summary>
        ///  Gets length of the played Wave
        /// </summary>
        public int Length
        {
            get { return Convert.ToInt32(wav.SampleLength); }
        }

        /// <summary>
        ///  Maximum value of sample in input wave
        /// </summary>
        private int maxValue;

        #endregion

        #region buffer_fields
        // fields necessary for buffering

        /// <summary>
        ///  Stores buffer data.
        /// </summary>
        private double[] bufferArray;

        // size of buffer
        /// <summary>
        ///  Size of the <see cref="EffectStream.bufferArray"/>.
        /// </summary>
        private int bufferSize;

        /// <summary>
        ///  Count of the currently buffered samples.
        /// </summary>
        private int bufferdCount;

        /// <summary>
        ///  Write index to the <see cref="EffectStream.bufferArray"/>.
        /// </summary>
        private int writePos;

        /// <summary>
        ///  Read index to the <see cref="EffectStream.bufferArray"/>.
        /// </summary>
        private int readPos;

        // stores how many samples are needed to be bufferd at the moment
        /// <summary>
        ///  Indicates number of the samples that are to be buffered at the moment. (In other words: How many samples are necessary to fill the buffer at the moment)
        /// </summary>
        private int toBuffer;

        /// <summary>
        ///  Timer to keep buffer filled with data.
        /// </summary>
        private System.Timers.Timer timer;

        /// <summary>
        ///  locks the data when, adding or retrieving
        /// </summary>
        private object locker;

        #endregion

        #region constructor

        /// <summary>
        ///  Creates new instance that wraps given <see cref="Wave"/>.
        /// </summary>
        /// <param name="wav"> Input signal </param>
        /// <param name="effects"> EffectBoards to apply </param>
        /// <param name="timerInterval"> Interval of the  </param>
        /// <param name="bufferSize"> Desired buffer size </param>
        public EffectStream(Wave.Wave wav,EffectBoard[] effects,int timerInterval,int bufferSize)
        {
            locker = new object();
            
            this.wav = wav;
            readPosition = 0;
            channels = wav.FmtChunk.Channels;

            this.effects = effects;
            //buffered = new List<double>();
            this.worksize = 128;

            for (int channel = 0; channel < channels; channel++)
            {
                this.effects[channel].SetDataSize(worksize);
                this.effects[channel].SetSampleRate(wav.FmtChunk.SampleRate);
            }
            
            //this.bufferSize = bufferSize/wav.FmtChunk.FrameSize;
            // set buffer length to be equal to one 20th of second
            
            //this.bufferSize = Convert.ToInt32(wav.FmtChunk.SampleRate / 20);
            this.bufferSize = Convert.ToInt32(wav.FmtChunk.SampleRate * wav.FmtChunk.Channels);
            bufferArray = new double[this.bufferSize];
            writePos = 0;
            readPos = 0;
            bufferdCount = 0;

            //prepares timer
            timer = new System.Timers.Timer(5);
            timer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerElapsed);

            // prepare maximal value
            maxValue = Convert.ToInt32(Math.Pow(2,wav.FmtChunk.BitsPerSample - 1));
            
            // reset
            Stop();
        }

        #endregion

        #region conversions
        // conversion from doubles to bytes

        /// <summary>
        ///  Converts double value to byte. Used with 8 bit bit-depth.
        /// </summary>
        /// <param name="d"> double value </param>
        /// <returns> byte value. </returns>
        private byte toByte(double d)
        {
            byte ret = Convert.ToByte(128 + 127 * d);
            return ret;
        }

        /// <summary>
        ///  Converts double array to byte array according to wav.FmtChunk, som returned array can be played or writen to file as it is.
        /// </summary>
        /// <param name="data"> Signal as doubles, correctly interleaved</param>
        /// <returns> Signal as bytes </returns>
        private byte[] ToByteArray(double[] data)
        {
            byte[] retData = new byte[data.Length * wav.FmtChunk.FrameSize];
            int index = 0;

           /* int maxValue = 1;
            for (int i = 0; i < wav.FmtChunk.BitsPerSample / 8; i++)
                maxValue *= 256;
            maxValue /= 2; */

            for (int i = 0; i < data.Length; i++)
            {
                if (maxValue == 128)
                {
                    retData[index++] = toByte(data[i]);
                }
                else
                {
                    int tmp = (int)(data[i] * maxValue);
                    byte[] buffer = BitConverter.GetBytes(tmp);

                    if (!BitConverter.IsLittleEndian)
                        Array.Reverse(buffer);

                    for (int j = 0; j < wav.FmtChunk.BitsPerSample / 8; j++)
                        retData[index++] = buffer[j];
                }
            }

            return retData;
        }

        #endregion

        #region buffer_work
        // work with buffer - adding, retrieving

        /// <summary>
        ///  Timer event handler.  Ensures that buffer is filled
        /// </summary>
        /// <param name="sender"> Object that has raised the event </param>
        /// <param name="e"> Event arguments </param>
        private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // check readPosition
            if (readPosition >= Length)
            {
                timer.Enabled = false;
                readPosition = Length;
            }
            else
            {
                // Fill buffer if needed
                FillBuffer();
            }
        }

        /// <summary>
        ///  Deprecated.
        /// </summary>
        /// <param name="bytes"> amount of bytes to prepare to the  buffer. </param>
        public void Buffer(int bytes)
        {
            toBuffer += bytes / wav.FmtChunk.FrameSize;
        }

        /// <summary>
        ///  Retrieves output signal from buffer.
        /// </summary>
        /// <param name="bytes"> Number of bytes to retrieve </param>
        /// <returns> Output signal </returns>
        public byte[] Get(int bytes)
        {

            //Console.WriteLine("Get");
            double[] retData = null;
            int samples = 8 * bytes / wav.FmtChunk.BitsPerSample;

            retData = new double[samples];

            // locks locker and gets data from buffer
            lock (locker)
            {
                try
                {
                    //retData = buffered.GetRange(0, samples).ToArray();
                    //buffered.RemoveRange(0, samples);


                    int space = bufferSize - readPos;
                    int untilBufferEnd = samples >= space ? space : samples;

                    for (int i = 0; i < untilBufferEnd; i++)
                    {
                        retData[i] = bufferArray[readPos];
                        readPos++;
                    }

                    int afterBufferEnd = samples - untilBufferEnd;
                    if (afterBufferEnd > 0)
                    {
                        readPos = 0;
                        for (int i = untilBufferEnd; i < samples; i++)
                        {
                            retData[i] = bufferArray[readPos];
                            readPos++;
                        }
                    }

                    bufferdCount -= samples;

                }
                catch (ArgumentException)
                {
                    retData = new double[samples];
                }
                catch (IndexOutOfRangeException)
                {
                    //int bla = 0;
                }

            }

            // tells the stream that more data need to be buffered
            toBuffer += bytes / wav.FmtChunk.FrameSize;

            // convert to byte and return
            return ToByteArray(retData);
        }

        /// <summary>
        ///  Fills buffer
        /// </summary>
        private void FillBuffer()
        {
            if (readPosition < Length)
            {
                while (toBuffer > worksize)
                {
                    //Console.WriteLine("Stream buffering, to buffer: {0}, Read position: {1}",toBuffer,readPosition);
                    double[][] buffer = new double[channels][];

                    for (int channel = 0; channel < channels; channel++)
                    {
                        int currentRead = readPosition;

                        buffer[channel] = new double[worksize];
                        for (int i = 0; i < worksize; i++)
                        {
                            try
                            {
                                buffer[channel][i] = wav.getSample(channel, currentRead++);
                            }
                            catch (ArgumentException)
                            {
                                buffer[channel][i] = 0;
                            }
                        }

                        buffer[channel] = effects[channel].Process(buffer[channel]);
                    }

                    readPosition += worksize;
                    //buffer = effects.Process(buffer);

                    double[] interleave = new double[worksize * channels];
                    for (int channel = 0; channel < channels; channel++)
                        for (int i = 0; i < worksize; i++)
                        {
                            interleave[channels * i + channel] = buffer[channel][i];
                        }

                    lock (locker)
                    {

                        WritoToBuffer(interleave);
                        toBuffer -= worksize;
                    }
                }
            }
        }

        /// <summary>
        ///   Writes given data to buffer
        /// </summary>
        /// <param name="data"> Data to be buffered </param>
        private void WritoToBuffer(double[] data)
        {
            try
            {
                int space = bufferSize - writePos;
                int untilBufferEnd = data.Length >= space ? space : data.Length;

                for (int i = 0; i < untilBufferEnd; i++)
                {
                    bufferArray[writePos] = data[i];
                    writePos++;
                }

                int afterBufferEnd = data.Length - untilBufferEnd;
                if (afterBufferEnd > 0)
                {
                    writePos = 0;
                    for (int i = untilBufferEnd; i < data.Length; i++)
                    {
                        bufferArray[writePos] = data[i];
                        writePos++;
                    }
                }

                bufferdCount += data.Length;
            }
            catch (IndexOutOfRangeException)
            {
                //int bla = 0;
            }
        }
        
#endregion

        #region commands
        // play_pause_stop commands

        /// <summary>
        ///  Stops the stream. Sets read position to the beginning of the file and prepares the buffer.
        /// </summary>
        public void Stop()
        {
            // stops timer.
            timer.Enabled = false;

            // reset inpu position
            readPosition = 0;

            // fills buffer
            lock (locker)
            {
                bufferArray = new double[bufferSize];
            }

            toBuffer = bufferSize / 20;
            FillBuffer();
        }

        /// <summary>
        ///  Starts the stream.
        /// </summary>
        public void Start()
        {
            // runs the timer to keep buffer filled
            timer.Enabled = true;
        }

        /// <summary>
        ///  Pauses the stream.
        /// </summary>
        public void Pause()
        {
            // stops timer, but doesn't reset input position
            timer.Enabled = false;
        }

        // method resets the stream. It moves read position to the beginning and starts the stream.
        /// <summary>
        ///  Resets the stream. Sets read position to the beginning of the file, prepares the buffer and starts the stream.
        /// </summary>
        public void Reset()
        {
            Stop();
            Start();

        }

        #endregion
    }
}
